/*
 Copyright 2014 Alexander V. Nikulin
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
	
 	http://www.apache.org/licenses/LICENSE-2.0
	
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
*/
package ru.avn.adf;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import net.sf.cglib.reflect.FastClass;
import net.sf.cglib.reflect.FastMethod;

/**
 * @author Alexander V. Nikulin
 */
public abstract class PayloadBodyExtractorFactory {

	public static PayloadBodyExtractorFactory getFactory(Class<?> classType) {
		return new CglibPayloadExtractorFactory(classType);
	}
	
	public abstract PayloadBodyExtractor getPayloadBodyExtractor(PayloadDeclaration<? extends Annotation> declaration);
	
	private static class SelfPayloadBodyExtractorHolder {
		protected static SelfPayloadBodyExtractor INSTANCE = new SelfPayloadBodyExtractor();
	}
	
	private static class SelfPayloadBodyExtractor implements PayloadBodyExtractor {

		@Override
		public Object extract(Object extractFrom) {
			return extractFrom;
		}

		@Override
		public void load(Object object, Object loadTo) {
			throw new UnsupportedOperationException("Unable to load object into itself");
		}
		
	}
	
	
	public static class CglibPayloadExtractorFactory extends PayloadBodyExtractorFactory {

		private FastClass fastClass;
		
		private Map<String, PayloadBodyExtractor> readyExtractors = Collections.<String, PayloadBodyExtractor>emptyMap();
		
		private Object[] emptyArgs = new Object[] {};
	
		public CglibPayloadExtractorFactory(Class<?> classType) {
			this.fastClass = FastClass.create(classType);
		}

		private Map<String, PayloadBodyExtractor> createMap() {
			return new HashMap<String, PayloadBodyExtractor>();
		}
		
		@Override
		public PayloadBodyExtractor getPayloadBodyExtractor(PayloadDeclaration<? extends Annotation> declaration)  {
			
			PayloadBodyExtractor extractor = readyExtractors.get(declaration.getName());
			
			if (extractor != null) {
				return extractor;
			}

			extractor = createPayloadBodyExtractor(declaration);
			
			if (readyExtractors.isEmpty()) {
				readyExtractors = createMap();
			}
			
			readyExtractors.put(declaration.getName(), extractor);
			
			return extractor;			
		}
		
		private PayloadBodyExtractor createPayloadBodyExtractor(PayloadDeclaration<? extends Annotation> declaration) {
			
			if (declaration.isClass()) {
				return SelfPayloadBodyExtractorHolder.INSTANCE;
			}
					
			Method getterMethod = ReflectionUtils.getGetter(declaration);
			Method setterMethod = ReflectionUtils.getSetter(declaration);
			
			if (getterMethod == null && setterMethod == null) {
				throw new IllegalStateException("Getter or Setter method must be present for declaration " + declaration.getName() + " in class " + declaration.getContainerType());
			}
		
			final MethodInvoker getter = createNullSafetyMethodInvoker(getterMethod);
			final MethodInvoker setter = createNullSafetyMethodInvoker(setterMethod);
									
			return new PayloadBodyExtractor() {
				@Override
				public void load(Object object, Object loadTo) {
					try {
						setter.invoke(loadTo, new Object[] {object});
					} catch (Exception e) {
						throw new RuntimeException(e);
					}
				}
				
				@Override
				public Object extract(Object extractFrom) {
					try {
						return getter.invoke(extractFrom, emptyArgs);
					} catch (Exception e) {
						throw new RuntimeException(e);
					}
				}
			};
		
		}
		
		private MethodInvoker createNullSafetyMethodInvoker(Method method) {
			MethodInvoker methodInvoker = null;
			
			if (method == null) {
				methodInvoker = MethodInvokers.getNullMethodInvoker();
			} else {
				final FastMethod fastMethod = fastClass.getMethod(method);
				
				methodInvoker = new MethodInvoker() {
					@Override
					public Object invoke(Object targetObject, Object... args) {
						try {
					
							return fastMethod.invoke(targetObject, args);
						} catch (InvocationTargetException e) {
							throw new RuntimeException(e);
						}
					}
				};
				
			}
			
			return methodInvoker;
		}		
		
	}
	


	
}
